Creational Design Patterns
Design pattern adalah solusi efektif untuk masalah yang berulang dalam pengembangan perangkat lunak.
Sebuah pattern bukan kode yang langsung di-copy-paste — melainkan blueprint yang perlu disesuaikan dengan konteks spesifikmu. Satu pattern bisa memecahkan beberapa masalah, tapi tidak setiap masalah membutuhkan pattern.
Sumber: Refactoring Guru
Singleton
Memastikan sebuah kelas hanya memiliki satu instansi, dan menyediakan titik akses global ke instansi tersebut.
Explorer
src
problem1-singleton
databaseConnection.before.ts
databaseConnection.ts
userRepository.before.ts
userRepository.ts
productRepository.before.ts
productRepository.ts
transactionRepository.before.ts
transactionRepository.ts
demo.ts
src/problem1-singleton/databaseConnection.ts
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export class DatabaseConnection {
private static maxConnectionPool: number = 2;
private static currentConnectionPool: number = 0;
private static _instance: DatabaseConnection | null = null;
private constructor() {
if (this.isConnectionFull()) throw Error("Connection pool is full");
this.init();
}
// Intinya ProductRepository, TransactionRepository, UserRepository langsung ambil instance
// tanpa harus buat instance baru lagi, karena sudah ada instance yang dibuat di constructor DatabaseConnection
public static getInstance(): DatabaseConnection {
if (this._instance === null) {
this._instance = new DatabaseConnection();
}
return this._instance;
}
private async init() {
console.log("Starting connection...");
console.log("Connecting to database ...");
console.log("✓ Connection created");
this.increaseCurrentConnnectionPool();
}
private increaseCurrentConnnectionPool() {
DatabaseConnection.currentConnectionPool++;
}
private isConnectionFull(): boolean {
return (
DatabaseConnection.currentConnectionPool ===
DatabaseConnection.maxConnectionPool
);
}
}
Masalah — Setiap repository membuat koneksi baru
export class DatabaseConnection {
private static maxConnectionPool: number = 2;
private static currentConnectionPool: number = 0;
constructor() { // constructor PUBLIC — bisa dibuat berkali-kali
if (this.isConnectionFull()) throw Error("Connection pool is full");
this.init();
}
// ...
}
// ❌ Setiap repository membuat koneksi barunya sendiri
export class UserRepository {
private _db: DatabaseConnection;
constructor() {
this._db = new DatabaseConnection(); // koneksi baru
}
}
export class ProductRepository {
private _db: DatabaseConnection;
constructor() {
this._db = new DatabaseConnection(); // koneksi baru lagi
}
}
Solusi — Semua repository berbagi satu koneksi
export class DatabaseConnection {
private static _instance: DatabaseConnection | null = null;
private constructor() { // constructor PRIVATE — tidak bisa dibuat dari luar
if (this.isConnectionFull()) throw Error("Connection pool is full");
this.init();
}
public static getInstance(): DatabaseConnection {
if (this._instance === null) {
this._instance = new DatabaseConnection();
}
return this._instance; // selalu mengembalikan instansi yang sama
}
// ...
}
// ✅ Semua repository berbagi satu koneksi
export class UserRepository {
private _db: DatabaseConnection;
constructor() {
this._db = DatabaseConnection.getInstance();
}
}
export class ProductRepository {
private _db: DatabaseConnection;
constructor() {
this._db = DatabaseConnection.getInstance();
}
}
Kapan Digunakan
- Kamu butuh tepat satu instansi yang dibagi ke seluruh aplikasi (misal: koneksi DB, config, logger)
- Membuat objeknya membutuhkan proses yang mahal dan sebaiknya hanya dilakukan sekali
Builder
Membangun objek yang kompleks secara bertahap, memisahkan proses konstruksi dari objek akhirnya.
Explorer
src
problem2-builder
iQueryBuilder.new.ts
query.ts
QueryBuilder.ts
demo.before.ts
demo.ts
src/problem2-builder/QueryBuilder.ts
// Ini file baru
import { IQueryBuilder } from "./iQueryBuilder.new";
import { Query } from "./query";
export class QueryBuilder implements IQueryBuilder {
private _table: string;
private _selectFields: string[];
private _whereClauses: string[];
constructor(table: string, selectFields: string[] = ["*"]) {
this._table = table;
this._selectFields = selectFields;
this._whereClauses = [];
}
setTable(table: string): IQueryBuilder {
this._table = table;
return this;
}
setSelectFields(fields: string[]): IQueryBuilder {
this._selectFields = fields;
return this;
}
setWhereClauses(clauses: string[]): IQueryBuilder {
this._whereClauses = clauses;
return this;
}
get(): Query {
return new Query(this._table, this._selectFields, this._whereClauses);
}
}
Masalah — Constructor yang membingungkan
import { Query } from "./query";
// Susah dibaca — argumen apa saja ini?
const query = new Query("users", ["id", "username", "email"], []);
query.print();
// ❌ Object bisa dimodifikasi langsung dari luar
query.table = "";
query.print();
Solusi — Builder yang bisa di-chain
import { QueryBuilder } from "./QueryBuilder";
// Mudah dibaca — setiap langkah jelas tujuannya
const query = new QueryBuilder("users", ["id", "username", "email"])
.setWhereClauses(["age > 18"])
.get();
query.print();
// ✅ Object tidak bisa dimodifikasi sembarangan
// query.table = ""; // tidak perlu lagi
Kapan Digunakan
- Membangun objek dengan banyak konfigurasi opsional
- Kamu ingin proses konstruksi yang mudah dibaca, langkah demi langkah
- Proses konstruksi yang sama harus bisa menghasilkan representasi yang berbeda
Prototype
Membuat objek baru dengan mengkloning objek yang sudah ada, daripada membangun dari awal.
Explorer
src
problem3-prototype
iClonable.new.ts
invoice.before.ts
invoice.ts
demo.before.ts
demo.ts
src/problem3-prototype/invoice.ts
import { IClonable } from "./iClonable.new";
export class Invoice implements IClonable {
private _companyName: string;
private _logo: string;
private _footer: string;
private _date: Date;
private _clientName: string;
private _items: string[];
private _totalAmount: number;
constructor(
companyName: string, logo: string, footer: string,
date: Date, clientName: string, items: string[], totalAmount: number,
) {
console.log("Initializing new invoice...");
console.log("Loading invoice layout template...");
console.log("Loading company logo and footer...");
console.log("Setting up styles and fonts...");
console.log("✓ Invoice base setup complete.\n");
this._companyName = companyName;
this._logo = logo;
this._footer = footer;
this._date = date;
this._clientName = clientName;
this._items = items;
this._totalAmount = totalAmount;
}
clone(): Invoice {
return new Invoice(
this._companyName, this._logo, this._footer,
this._date, this._clientName, this._items, this._totalAmount,
);
}
// Getters & Setters
get clientName(): string { return this._clientName; }
get items(): string[] { return this._items; }
get totalAmount(): number { return this._totalAmount; }
set clientName(value: string) { this._clientName = value; }
set items(value: string[]) { this._items = value; }
set totalAmount(value: number) { this._totalAmount = value; }
public print() {
console.log(`===== INVOICE FOR ${this._clientName} =====`);
console.log(`Company: ${this._companyName}`);
console.log(`Date: ${this._date.toDateString()}`);
console.log(`Items: ${this._items.join(", ")}`);
console.log(`Total: $${this._totalAmount}`);
console.log(`${this._footer}`);
console.log("=".repeat(100));
}
}
Masalah — Setup mahal diulang terus
// Setiap invoice menjalankan constructor yang berat
const invoice1: Invoice = new Invoice(
"ABC Corp", "company_logo.png", "Thank you for your business!",
new Date(), "Client A", ["Laptop", "Mouse"], 1200,
);
// "Initializing new invoice..."
// "Loading invoice layout template..."
// "Loading company logo and footer..."
// "Setting up styles and fonts..."
const invoice2: Invoice = new Invoice(
"ABC Corp", "company_logo.png", "Thank you for your business!",
new Date(), "Client B", ["Monitor", "Keyboard"], 800,
);
// setup yang sama diulang lagi...
const invoice3: Invoice = new Invoice(
"ABC Corp", "company_logo.png", "Thank you for your business!",
new Date(), "Client C", ["Desk Chair"], 300,
);
// dan lagi...
Solusi — Clone lalu modifikasi
// Setup berat hanya dijalankan sekali
const invoice1: Invoice = new Invoice(
"ABC Corp", "company_logo.png", "Thank you for your business!",
new Date(), "Client A", ["Laptop", "Mouse"], 1200,
);
// Clone melewati constructor — hanya ubah yang berbeda
const invoice2: Invoice = invoice1.clone();
invoice2.clientName = "Client B";
invoice2.items = ["Monitor", "Keyboard"];
invoice2.totalAmount = 800;
const invoice3: Invoice = invoice1.clone();
invoice3.clientName = "Client C";
invoice3.items = ["Desk Chair"];
invoice3.totalAmount = 300;
Kapan Digunakan
- Pembuatan objek membutuhkan proses yang mahal (setup berat, panggilan DB, loading file)
- Kamu butuh banyak objek serupa yang hanya berbeda di beberapa properti
- Kamu ingin menghindari subclassing hanya untuk mendapatkan state awal yang berbeda
Ringkasan
| Pattern | Tujuan | Mekanisme Utama |
|---|---|---|
| Singleton | Hanya satu instansi, dibagi secara global | Constructor private + getInstance() |
| Builder | Membangun objek kompleks secara bertahap | Method berantai + get() |
| Prototype | Mengkloning objek yang ada daripada membuatnya | Method clone() |